#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "lrc.h"
#include "./pos/console.h"

// extern 关键字声明：m_time 是一个 TIME 类型的全局变量，它在其他文件中定义。
// TIME 类型在 "lrc.h" 中定义，用于存储分和秒。
extern TIME m_time;
// extern 关键字声明：song_msg 是一个包含4个 char* (字符串指针) 的数组。
// 它用于存储歌曲的前四行信息（如歌名、歌手等）的内存地址。它也在其他文件中定义。
extern char * song_msg[4]; //保存前四行歌曲信息首地址

/*************************************************************************
*	函数功能:读出文件内容
*	参数： 
* 		src_file_name:文件名字，从此文件中读取内容。 
*	返回值:读出字符串的首地址
**************************************************************************/
// 函数定义：read_src_file
// 功能：读取指定文件的全部内容，并将其存储在一个动态分配的字符串中。
// 参数：
//   char *src_file_name: 一个指向字符的指针，表示要读取的文件的路径和名称。
//                        在C语言中，字符串通常用 char* 表示。
// 返回值：
//   char*: 读取成功时，返回一个指向新分配的内存区域的指针，该区域包含文件的文本内容。
//          字符串以 '\0' (空字符) 结尾。
//          读取失败（如文件打开失败、读取错误、内存分配失败），则返回 NULL (空指针)。
char * read_src_file(char *src_file_name)
{
	// 声明一个无符号长整型变量 FILE_SIZE，并初始化为0。用于存储文件的大小。
	unsigned long int FILE_SIZE=0;
	// 声明一个字符指针 src_file_text，并初始化为 NULL。它将指向存储文件内容的内存。
	char *src_file_text=NULL;
	// 声明一个文件指针 fp，并初始化为 NULL。FILE 是一个在 <stdio.h> 中定义的结构体，用于表示文件流。
	FILE  *fp=NULL;

	// 调用 fopen 函数尝试以 "rb" (二进制只读) 模式打开指定名称的文件。
	// "r" 表示只读，"b" 表示二进制模式（对于文本文件和二进制文件，"b" 在某些系统上行为可能不同，但通常是好的实践）。
	// 如果 fopen 成功，它返回一个指向 FILE 对象的指针；如果失败，返回 NULL。
	fp=fopen(src_file_name,"rb");
	// 检查文件是否成功打开。NULL 是一个特殊的指针值，表示指针不指向任何有效的内存地址。
	if(fp==NULL)
	{
		// 如果文件打开失败，使用 printf 函数打印错误信息到控制台。
		// %s 是一个格式说明符，表示此处将打印一个字符串。
		// \n 是一个转义字符，表示换行。
		printf("open %s faild\n",src_file_name);
		// 返回 0 (在指针上下文中，0 通常等同于 NULL)，表示函数执行失败。
		return 0;
	}

	// 调用 fseek 函数将文件指针 fp 移动到文件的末尾。
	// SEEK_END: 表示从文件末尾开始计算偏移量。
	// 0: 表示偏移量为0字节。
	// 这样，文件指针就位于文件的最后一个字节之后。
	fseek(fp,0,SEEK_END);
	// 调用 ftell 函数获取文件指针 fp 当前相对于文件开头的位置（以字节为单位）。
	// 由于上一步已将指针移到文件末尾，所以这里得到的就是文件的总大小。
	FILE_SIZE=ftell(fp);

	// 调用 rewind 函数将文件指针 fp 重新移动到文件的开头。
	// 这样后续的 fread 就能从文件开始处读取。
	rewind(fp);					//一定要把"文件指针"移动到文件开始处

	// 调用 malloc 函数动态分配内存，用于存储文件内容。
	// FILE_SIZE + 1: 需要 文件大小的字节数来存储内容，再加上1个字节用于存放字符串结束符 '\0'。
	// (char *): malloc 返回的是 void* 类型指针，这里将其强制类型转换为 char* (字符指针)。
	src_file_text=(char *)malloc(FILE_SIZE+1);
	// 检查内存是否分配成功。如果 malloc 失败 (例如系统内存不足)，它会返回 NULL。
	if(src_file_text == NULL) {
		printf("malloc for src_file_text failed\n");
		fclose(fp); // 关闭已打开的文件，防止资源泄露
		return NULL; // 返回 NULL 表示失败
	}

	// 调用 memset 函数将刚分配的内存区域 src_file_text 初始化为全零字节 ('\0')。
	// '\0': 要设置的字符。
	// FILE_SIZE + 1: 要设置的字节数。
	// 这样做可以确保字符串正确结束，即使 fread 没有填满整个缓冲区。
	memset(src_file_text,'\0',FILE_SIZE+1);

	// 调用 fread 函数从文件 fp 中读取数据到 src_file_text 指向的内存中。
	// src_file_text: 存储读取数据的目标内存地址。
	// FILE_SIZE: 要读取的每个数据块的大小（这里是整个文件内容一次性读取，所以是 FILE_SIZE）。
	// 1: 要读取的数据块的数量（这里是1块）。
	// fp: 文件指针。
	// fread 返回成功读取的数据块的数量。如果返回值不为1 (或为0，因为我们只尝试读1块)，表示读取失败或未完整读取。
	if(fread(src_file_text,FILE_SIZE,1,fp)==0)
	{
		// 如果读取失败，打印错误信息。
		printf("read from src_file faild\n");
		free(src_file_text); // 释放之前分配的内存，防止内存泄露
		fclose(fp); // 关闭文件
		return NULL; // 返回 NULL 表示失败
	}

	// 调用 fclose 函数关闭文件。释放与文件指针 fp 相关联的资源。
	fclose(fp);
	// 返回指向存储文件内容的字符串的指针。
	return src_file_text ;
}
/************************************************************************
*	函数功能:切割 歌词数组
*	参数：
*		lrc_text 指针数组，让其指针指向切出来的内容 
* 		file_buf 歌词内容数组
* 	返回值:切割出来的字符串的个数
*************************************************************************/
// 函数定义：strtok_lrc_buf
// 功能：将一个包含多行歌词的字符串 (file_buf) 按照换行符 ("\r\n") 切割成多行，
//       并将每一行的起始地址存储在指针数组 lrc_text 中。
// 参数：
//   char *lrc_text[]: 一个字符指针数组。数组的每个元素都是一个 char*，
//                     用于指向被切分出来的每一行歌词字符串的起始地址。
//                     这个数组需要由调用者预先分配足够的空间。
//   char *file_buf: 一个指向包含完整歌词文本的字符串的指针。
//                   注意：strtok 函数会修改它所处理的字符串 (file_buf)，
//                   它会在找到的分隔符位置插入 '\0'。
// 返回值：
//   int: 切割出来的歌词的总行数。
int strtok_lrc_buf(char *lrc_text[],char *file_buf)
{
	// 声明一个整型变量 i，并初始化为0。用作 lrc_text 数组的索引，也用于计数行数。
	int i=0;

	// 调用 strtok 函数来切分字符串 file_buf。
	// 第一次调用 strtok 时，第一个参数是要被切分的字符串 file_buf。
	// 第二个参数是分隔符字符串，这里是 "\r\n"，表示回车符 (\r) 或换行符 (\n) 都作为分隔符。
	// strtok 会找到第一个分隔符，将其替换为 '\0'，并返回指向该子字符串（即第一行）的指针。
	// 如果未找到分隔符，则返回原始字符串的指针（如果字符串不为空）。
	// 如果输入字符串本身为 NULL 或者没有更多 token，则返回 NULL。
	lrc_text[i]=strtok(file_buf,"\r\n");

	// 使用 while 循环继续切分字符串，直到 strtok 返回 NULL。
	// 当 strtok 返回 NULL 时，表示已经没有更多的子字符串可以被切分了。
	// lrc_text[i] != NULL 检查当前是否成功获取到一个子字符串。
	while(lrc_text[i]!=NULL)
	{
		// 在循环中，后续调用 strtok 时，第一个参数必须是 NULL。
		// 这告诉 strtok 继续在上一次调用中处理的同一个字符串 (file_buf) 上查找下一个分隔符。
		// ++i: 先将 i 增加 1，然后将返回的下一行歌词的指针存入 lrc_text[i]。
		lrc_text[++i]=strtok(NULL,"\r\n");
	}
	// 返回切割出来的总行数 i。
	return i;
}


/**************************************************************************
*	函数功能:解析lrc前四行，歌曲的信息如歌手、专辑等。。
*	参数：
*		lrc_text 指针数组 lrc 中的前四行 
*		song_msg 指针数组 用来指向处理好的歌曲信息
* 	返回值:无
**************************************************************************/
// 函数定义：lrc_head_resolve
// 功能：解析 LRC 歌词文件的前四行，提取歌曲的元信息（如歌名、歌手、专辑、制作者）。
//      这些信息通常以特定标签开头，如 "[ti:" (歌名), "[ar:" (歌手) 等。
// 参数：
//   char *song_msg[]: 一个字符指针数组，大小为4。用于存储解析后提取出来的歌曲信息字符串的指针。
//                     例如 song_msg[0] 可能指向歌名字符串。如果某项信息不存在，对应的指针会被设为 NULL。
//                     该函数会为提取出的信息动态分配内存。
//   char *lrc_text[]: 一个字符指针数组，包含 LRC 文件的前四行内容的指针。
//                     lrc_text[0] 指向第一行，lrc_text[1] 指向第二行，以此类推。
// 返回值：
//   void: 该函数没有返回值 (void 表示不返回任何值)。它直接修改 song_msg 数组的内容。
void lrc_head_resolve(char *song_msg[],char * lrc_text[])
{
	// 声明一个整型变量 i，用于循环计数。
	int i=0;
	// 定义一个字符指针数组 message，存储 LRC 文件头部信息的标准标签。
	// message[0] = "[ti" (title - 歌名)
	// message[1] = "[ar" (artist - 歌手)
	// message[2] = "[al" (album - 专辑)
	// message[3] = "[by" (by - 制作者/LRC歌词编辑者)
	char * message[]={"[ti","[ar","[al","[by"};

	// 使用 for 循环遍历前四行歌词信息 (i 从 0 到 3)。
	for(i=0;i<4;i++)// 循环四次 找到四个头信息的开始
	{
		// 检查 lrc_text[i] (当前行) 是否为 NULL，以及其长度是否足够进行比较和提取。
		// 这是一个防御性编程的补充，原代码没有此检查。
		if (lrc_text[i] == NULL || strlen(lrc_text[i]) < 5) { // 至少需要像 "[ti:]" 这样的长度
			song_msg[i] = NULL;
			continue; // 处理下一行
		}
		// 使用 strncmp 函数比较当前行 lrc_text[i] 的开头部分是否与预期的标签 message[i] 匹配。
		// strncmp(s1, s2, n) 比较字符串 s1 和 s2 的前 n 个字符。
		// 这里比较前2个字符 (例如，"[t" 和 "[t")。
		// 如果匹配 (返回值为0)，则认为找到了对应的歌曲信息。
		if (strncmp(lrc_text[i],message[i],2)==0)
		{
			// 如果标签匹配成功，为存储该项歌曲信息动态分配内存。
			// strlen(lrc_text[i]) 获取当前歌词行的总长度。
			// 减去4是为了去除标签部分（如 "[ti:"）和结尾的 "]"。长度计算可能需要更精确，
			// 例如 strlen(lrc_text[i]) - (strlen(message[i]) + 1 (for ':') + 1 (for ']') ) + 1 (for '\0')
			// 更安全的做法是先提取再分配。这里按原逻辑注释，但指出潜在问题。
			// 原代码的 strlen(lrc_text[i])-4 假设信息部分固定比总长少4，这可能不总是准确。
			// 一个更健壮的方法是先用 sscanf 提取，再用 strlen(提取内容) + 1 来分配。
			// 但这里我们先遵循原代码的逻辑来分配一个大致足够的空间。
			int info_len = strlen(lrc_text[i]) - strlen(message[i]) - 2; // 估算信息长度（去掉 "[xx:" 和 "]"）
			if (info_len < 0) info_len = 0; // 防止负数长度

			song_msg[i]=(char *)malloc(info_len + 1); // +1 for '\0'
			if(song_msg[i] == NULL){
				// 内存分配失败处理
				perror("malloc for song_msg[i] failed");
				// 理想情况下，这里应该做更复杂的错误处理，比如释放已分配的内存并返回错误码
				continue; // 跳过这一项
			}

			// 使用 sscanf 函数从 lrc_text[i] 字符串中提取实际的歌曲信息。
			// "%*4s" : %*s 表示忽略（不赋值）一个字符串。%*4s 表示忽略前4个字符 (例如 "[ti:")。
			//            注意：这假设标签总是类似 "[xy:" 这种4字符形式。
			// "%[^]]" : 这是一个格式控制符，表示读取并存储字符，直到遇到右方括号 ']' 为止。
			//            ^ 符号表示"非"，所以 %[^]] 表示读取不是 ']' 的所有字符。
			// song_msg[i]: 提取出的信息存储到这里。
			// 例如，如果 lrc_text[i] 是 "[ti:我的歌]", sscanf 会将 "我的歌" 存入 song_msg[i]。
			// sscanf 返回成功匹配和赋值的项数。
			if (sscanf(lrc_text[i],"%*4s%[^]]",song_msg[i]) != 1) {
				// 如果解析失败，或者没有内容，释放刚分配的内存，并将指针设为NULL
				free(song_msg[i]);
				song_msg[i] = NULL;
			}
			// 移除可能存在的前导或尾随空格 (可选的增强)
			// trim_whitespace(song_msg[i]);
		}
		else//如果没有这个歌曲信息，song_msg[i]=NULL
		{
			// 如果当前行 lrc_text[i] 的开头与预期的标签不匹配，
			// 则表示该项歌曲信息 (例如歌名、歌手等) 在此LRC文件中不存在或格式不符。
			// 将对应的 song_msg[i] 指针设置为 NULL。
			song_msg[i]=NULL;
		}
	}
}

/**************************************************************************
*	函数功能:打印歌曲信息
*	参数：
*		song_msg 歌曲信息 指针数组指向歌曲头信息内容
*	返回值:无
*	注意点：无
**************************************************************************/
// 函数定义：print_song_message
// 功能：在控制台的特定位置打印歌曲的头部信息（歌名、歌手、专辑、制作者）。
// 参数：
//   char *song_msg[]: 一个字符指针数组，包含之前由 lrc_head_resolve 函数解析出来的歌曲信息。
//                    song_msg[0] 是歌名，song_msg[1] 是歌手，等等。
//                    如果某项信息为 NULL，则不打印。
// 返回值：
//   void: 该函数没有返回值。
void print_song_message(char * song_msg[])
{
	// 声明一个整型变量 i，用于循环计数。
	int i=0;
	// 定义一个字符指针数组 name，存储各项歌曲信息对应的中文标签。
	// 这些标签将与 song_msg 数组中的信息一同打印。
	char *name[4]={"歌名:","歌手:","专辑:","制作:"};

	// 调用 clear_screen() 函数，清除控制台屏幕上的所有内容。
	// 这个函数通常在 "console.h" 中声明，并在 "console.c" 中实现，使用特定的控制台命令。
	clear_screen();
	// 调用 cusor_hide() 函数，隐藏控制台的光标。
	// 这样做可以使歌词显示更美观，避免光标闪烁。
	cusor_hide();
	// 调用 set_fg_color(COLOR_BLUE) 函数，设置后续打印文本的前景色（字体颜色）为蓝色。
	// COLOR_BLUE 是一个预定义的宏，通常在 "console.h" 中定义，代表蓝色的颜色代码。
	set_fg_color(COLOR_BLUE);

	// 使用 for 循环遍历歌曲信息的各个项目 (i 从 0 到 3)。
	for(i=0;i<4;i++)
	{
		// 检查当前歌曲信息项 song_msg[i] 是否存在 (即指针不是 NULL)。
		if(song_msg[i]!=NULL)
		{
			// 如果信息存在：
			// 调用 cusor_moveto(35, i+1) 函数，将光标移动到屏幕的第 (i+1) 行，第 35 列。
			// 这样可以格式化输出，使各项信息在屏幕上对齐。
			cusor_moveto(35,i+1);
			// 使用 printf 函数打印歌曲信息的中文标签 (name[i]) 和实际信息 (song_msg[i])。
			// "%s  %s\n": 格式字符串。第一个 %s 对应 name[i]，第二个 %s 对应 song_msg[i]。
			//              两个空格用于分隔标签和信息，\n 用于在打印后换行。
			printf("%s  %s\n",name[i],song_msg[i]);
		}
	}
}


/**************************************************************************
*	函数功能:解析歌曲信息
*	参数：
*		lrc_text 指向歌词内容(以行的方式存储)的指针数组
*		lrc_line 歌词的行数
* 	返回值: 解析后的LRC歌词链表的头指针
**************************************************************************/
// 函数定义：lrc_resolve
// 功能：解析 LRC 歌词文件的主要内容（时间标签和歌词文本），
//       并将每一句带时间的歌词构造成一个 LRC 结构体节点，然后将这些节点组织成一个按时间排序的双向链表。
// 参数：
//   int lrc_line: LRC 歌词文件的总行数（由 strtok_lrc_buf 函数得到）。
//   char *lrc_text[]: 一个字符指针数组，其中每个指针指向 LRC 文件中的一行歌词文本。
//                     （通常在 main 函数中，这是跳过了头部信息后的歌词行）。
// 返回值：
//   LRC*: 指向构建完成的歌词双向链表的头节点的指针。如果解析失败或没有有效歌词行，可能返回 NULL。
//         LRC 类型是一个结构体，在 "lrc.h" 中定义，包含时间、歌词和指向前后节点的指针。
LRC* lrc_resolve(int lrc_line,char * lrc_text[])
{
	// 声明链表头指针 head 和新节点指针 p_new，都初始化为 NULL。
	// head 将指向链表的第一个节点。p_new 用于临时存储新创建的节点。
	LRC *head=NULL,*p_new=NULL;
	// 声明循环计数器 i 和 j。
	int i=0,j=0;
	// 声明一个字符指针 lrc_str，用于在处理每一行歌词时，指向当前解析到的歌词文本部分。
	char *lrc_str;
	// 声明一个整型数组 time_buf，大小为10。
	// 用于临时存储从一行歌词中解析出来的所有时间标签对应的时间值（单位：秒）。
	// 假设一行歌词最多有10个时间标签。
	int time_buf[10];		//用来暂时记录歌词中的时间,单位为：秒 (原注释为毫秒，但代码中是秒)
	// 声明一个整型变量 time_num，并初始化为0。
	// 用于记录当前处理的这行歌词有多少个与之关联的时间标签。
	int time_num=0;
	// 声明两个整型变量 min 和 sec，用于临时存储从时间标签 "[mm:ss.xx]" 中解析出来的分钟和秒。
	int min,sec;

	// 使用 for 循环遍历 lrc_text 数组中的每一行歌词。
	// 循环从 i=4 开始，因为通常 LRC 文件的前4行是歌曲头部信息，已由 lrc_head_resolve 处理。
	// lrc_line 是歌词的总行数。
	for(i=4;i<lrc_line;i++)
	{
		// 将 lrc_str 指向当前要处理的歌词行 lrc_text[i]。
		lrc_str=lrc_text[i];
		// 在处理新的一行歌词之前，将 time_num（本行时间标签计数器）重置为0。
		time_num=0;

		// 检查当前行是否为空或者不以'['开头，如果是则跳过这一行
		if (lrc_str == NULL || *lrc_str != '[') {
			continue;
		}

		// 使用 while 循环解析一行歌词中所有的 "[mm:ss.xx]" 时间标签。
		// *lrc_str == '[' : 检查当前 lrc_str 指向的字符是否是左方括号 '['。
		//                   只要是，就认为可能是一个时间标签的开始。
		while(*lrc_str == '[')
		{
			// 检查 time_num 是否超出 time_buf 的容量
			if (time_num >= 10) {
				// 如果时间标签过多，打印错误信息并跳出内层while循环处理下一个时间标签
				fprintf(stderr, "Warning: Too many time tags in one line: %s\n", lrc_text[i]);
				// 将 lrc_str 指针向后移动，尝试跳过这个无法处理的时间标签部分
                // 这是一种简化的错误处理，理想情况可能需要更精确地跳过整个标签
                char* next_bracket = strchr(lrc_str, ']');
                if (next_bracket) {
                    lrc_str = next_bracket + 1;
                } else {
                    // 如果找不到右括号，说明这行格式有问题，不再继续解析此行的时间
                    goto next_line; // 使用 goto 跳转到外层 for 循环的下一次迭代
                }
                continue; // 继续 while 循环，尝试解析下一个标签
			}
			// 使用 sscanf 函数从 lrc_str 指向的字符串中解析时间。
			// "[%2d:%2d" : 格式字符串。
			//   '[' : 匹配并跳过字面上的左方括号。
			//   %2d : 读取最多2位数字并作为整数存入 min (分钟)。
			//   ':' : 匹配并跳过字面上的冒号。
			//   %2d : 读取最多2位数字并作为整数存入 sec (秒)。
			//   &min, &sec : 将解析出的值存入变量 min 和 sec 的内存地址。
			// sscanf 返回成功赋值的变量个数。如果不是2，说明格式不匹配。
			if (sscanf(lrc_str,"[%2d:%2d",&min,&sec) == 2)
			{
				// 如果成功解析出分钟和秒，则计算总秒数 (min * 60 + sec)，
				// 并将其存入 time_buf 数组的 time_num 位置。然后 time_num 自增1。
				time_buf[time_num++]=min*60+sec;
			} else {
				// 解析时间失败，可能不是标准的时间标签，尝试跳过此标签。
				// 找到第一个 ']'，然后从它后面继续。
				char* end_bracket = strchr(lrc_str, ']');
				if (end_bracket) {
					lrc_str = end_bracket + 1; // 移动到 ']' 之后
					// 检查 lrc_str 是否为空或不是 '[' 开头，如果是则跳出while循环
                    if (*lrc_str == '\0' || *lrc_str != '[') break;
					continue; // 继续解析下一个可能的标签
				} else {
					// 如果找不到 ']'，说明这行格式有问题，不再继续解析此行的时间
					goto next_line; // 使用 goto 跳转到外层 for 循环的下一次迭代
				}
			}

			// 将 lrc_str 指针向后移动10个字符。
			// 这是基于假设时间标签的固定格式为 "[mm:ss.xx]" (长度10)。
			// 如果时间标签格式不固定（例如 "[mm:ss]" 或 "[mm:ss.xxx]"），这里会有问题。
			// 一个更健壮的方法是查找 ']' 并从其后开始。
            // (上面的错误处理已部分改进了这一点)
			char* temp_next_lrc_str = strchr(lrc_str, ']');
            if (temp_next_lrc_str) {
                lrc_str = temp_next_lrc_str + 1;
            } else {
                // 如果找不到右括号，说明这行格式有问题，不再继续解析此行的时间
                goto next_line; // 使用 goto 跳转到外层 for 循环的下一次迭代
            }
		}//内层while结束: 解析完一行的所有时间后, lrc_str 指向的是实际的歌词文本部分。

		// 根据本行歌词解析出的时间标签个数 (time_num)，
		// 为每一个时间点创建一个 LRC 节点，并将歌词文本与该时间点关联。
		for(j=0;j<time_num;j++)
		{
			// 调用 malloc 函数为新的 LRC 结构体节点分配内存。
			// sizeof(LRC) 获取 LRC 结构体所需的确切字节数。
			// (LRC *) 是类型转换。
			p_new =  (LRC *)malloc(sizeof(LRC));
			// 检查内存分配是否成功。
			if(p_new == NULL){
				perror("malloc for LRC node failed");
				// 理想情况下，这里应该释放已构建的部分链表并返回错误
				// 为了简化，这里只打印错误并尝试继续（可能导致后续问题）
				continue; // 跳过创建这个节点
			}

			// 将 time_buf[j] (第j个解析出的时间，单位秒) 赋值给新节点的 time 成员。
			p_new->time = time_buf[j];
			// 使用 strcpy 函数将 lrc_str 指向的歌词文本复制到新节点的 lrc_buf 成员中。
			// lrc_buf 是 LRC 结构体中定义的字符数组，用于存储歌词。
			// strcpy(destination, source)
			// 需要确保 lrc_str 的长度不超过 p_new->lrc_buf 的容量 (在lrc.h中定义为200)。
			// 可以使用 strncpy 提高安全性：strncpy(p_new->lrc_buf, lrc_str, sizeof(p_new->lrc_buf) - 1);
            // p_new->lrc_buf[sizeof(p_new->lrc_buf) - 1] = '\0';
			if (lrc_str != NULL) { // 确保 lrc_str 不是 NULL
                strncpy(p_new->lrc_buf, lrc_str, sizeof(p_new->lrc_buf) - 1);
                p_new->lrc_buf[sizeof(p_new->lrc_buf) - 1] = '\0'; // 确保空字符结尾
            } else {
                p_new->lrc_buf[0] = '\0'; // 如果歌词部分为空，则设置为空字符串
            }

			// 调用 double_link_insert 函数将新创建的节点 p_new 插入到双向链表 head 中。
			// &head: 传入链表头指针的地址，因为插入操作可能会修改头指针 (例如当链表为空或新节点插在最前面时)。
			// 该函数会确保链表按时间 (p_new->time) 升序排列。
			double_link_insert(&head,p_new);
		}
		next_line:; // goto 跳转标签
	} //外层for循环结束
	// 返回构建好的歌词链表的头指针。
	return head;
}

// 函数定义：link_search
// 功能：在已按时间排序的歌词双向链表中查找特定时间点 (m_time) 对应的歌词节点。
// 参数：
//   LRC *head: 指向歌词链表头节点的指针。
//   int m_time: 要查找的时间点 (单位：秒)。
// 返回值：
//   LRC*: 如果找到时间完全匹配的歌词节点，则返回该节点的指针。
//         如果遍历完整个链表都没有找到时间完全匹配的节点，则返回 NULL。
LRC* link_search(LRC *head,int m_time)
{
	// 声明一个 LRC 类型的指针 pb (pointer current/previous)，并用链表头指针 head 初始化它。
	// pb 将用于遍历链表。
	LRC *pb=head;
	// 使用 while 循环遍历链表，条件是 pb 不为 NULL (即未到达链表末尾)。
	while(pb!=NULL)
	{
		// 检查当前节点 pb 的 time 成员是否与要查找的时间 m_time 相等。
		if(pb->time==m_time)
			// 如果时间匹配，返回当前节点 pb 的指针。
			return pb;
		else
			// 如果时间不匹配，将 pb 指向链表中的下一个节点 (pb->next)。
			pb=pb->next;
	}
	// 如果循环结束（pb 变为 NULL），意味着没有找到时间匹配的节点，返回 NULL。
	return NULL;
}

// 函数定义：double_link_insert
// 功能：向一个已按时间升序排列的双向链表中插入一个新的 LRC 节点 (p_new)，并保持链表的有序性。
// 参数：
//   LRC **p_head: 一个指向链表头指针的指针 (二级指针)。
//                 使用二级指针是因为函数可能需要修改头指针本身的值
//                 (例如，当链表为空或新节点插入到最前面时，头指针会改变)。
//   LRC *p_new: 指向要插入的新 LRC 节点的指针。p_new 节点的内容 (time, lrc_buf) 应该已经填充好。
//               p_new 的 front 和 next 指针将在此函数中被正确设置。
// 返回值：
//   void: 该函数没有返回值。
void double_link_insert(LRC **p_head,LRC *p_new)
{
	// 声明两个 LRC 类型的指针 pb (current/traverse) 和 pf (previous/follower)。
	LRC *pb,*pf;
	// pb 初始化为指向链表的头节点。*p_head 解引用二级指针，得到头指针的值。
	pb=*p_head;

	// 情况1：链表为空 (*p_head == NULL)。
	if(*p_head == NULL)
	{
		// 新节点 p_new 成为链表的唯一节点，即头节点。
		*p_head=p_new;
		// 新节点的 front 和 next 指针都设为 NULL，因为它既没有前驱也没有后继。
		p_new->front=NULL;
		p_new->next=NULL;
		// 插入完成，函数返回。
		return ;
	}

	// 情况2：链表不为空。遍历链表，找到合适的插入位置。
	// 循环条件：
	//   (p_new->time >= pb->time): 当新节点的时间大于或等于当前节点 pb 的时间时，继续向后找。
	//                             目标是找到第一个 pb，使得 pb->time > p_new->time，或者到达链表尾部。
	//   (pb->next!=NULL):         并且当前节点 pb 不是链表的最后一个节点。
	while((p_new->time >= pb->time) && (pb->next!=NULL) )
	{
		// 将 pb 移动到下一个节点。
		pb=pb->next ;
	}

	// 循环结束后，pb 指向的节点有两种可能：
	// 1. pb->time > p_new->time (找到了插入点，p_new 应该插在 pb 前面)
	// 2. pb 是链表的最后一个节点，并且 p_new->time >= pb->time (p_new 应该插在 pb 后面)

	// 判断 p_new->time 是否小于当前 pb->time。
	if(p_new->time < pb->time) // 这意味着 p_new 应该插入到 pb 节点的前面。
	{
		// 子情况 2.1：pb 就是链表的头节点 (*p_head)。
		// 这种情况下，p_new 应该成为新的头节点。
		if(pb==*p_head)
		{
			// 将新节点 p_new 插入到头节点 pb 之前。
			p_new->next=*p_head;   // p_new 的 next 指向原来的头节点。
			(*p_head)->front=p_new; // 原来的头节点的 front 指向 p_new。
			p_new->front=NULL;     // p_new 是新的头，所以它的 front 是 NULL。
			*p_head=p_new;         // 更新链表的头指针，使其指向 p_new。
		}
		// 子情况 2.2：pb 不是头节点，p_new 插入到链表的中间某个位置 (在 pf 和 pb 之间)。
		else
		{
			// pf 指向 pb 的前一个节点。
			pf=pb->front;

			// 执行插入操作：
			p_new->next=pb;   // p_new 的 next 指向 pb。
			p_new->front=pf;  // p_new 的 front 指向 pf。
			pf->next=p_new;   // pf 的 next 指向 p_new。
			pb->front=p_new;  // pb 的 front 指向 p_new。
		}
	}
	// else: p_new->time >= pb->time。这意味着 pb 是最后一个节点，或者 p_new->time 等于 pb->time 且已遍历到尾部
    //       (因为while循环条件是 p_new->time >= pb->time && pb->next != NULL)。
    //       所以，p_new 应该插入到 pb 节点的后面 (成为新的尾节点或与pb时间相同的最后一个)。
	else
	{
		// 将新节点 p_new 插入到链表末尾 (当前 pb 节点的后面)。
		pb->next=p_new;    // pb 的 next 指向 p_new。
		p_new->front=pb;   // p_new 的 front 指向 pb。
		p_new->next=NULL;  // p_new 成为新的尾节点，所以它的 next 是 NULL。
	}
}



/**************************************************************************
*	函数功能:回收链表
*	参数：
*		head 链表的头节点地址。   
*		
* 	返回值:无
**************************************************************************/
// 函数定义：link_free
// 功能：释放整个双向链表所占用的所有动态分配的内存。
// 参数：
//   LRC *head: 指向要被释放的链表的头节点的指针。
// 返回值：
//   void: 该函数没有返回值。
void link_free(LRC *head)
{
	// 声明一个 LRC 类型的指针 pb，用于临时存储当前要释放的节点。
	LRC *pb;
	// 使用 while 循环遍历链表，直到 head 变为 NULL (表示链表已空)。
	while(head!=NULL)
	{
		// 将 pb 指向当前的头节点 head。
		pb=head;
		// 将链表的头指针 head 移动到下一个节点 (head->next)。
		head=head->next;
		// 调用 free 函数释放 pb 指向的节点所占用的内存。
		// free() 只能用于释放由 malloc(), calloc(), realloc() 分配的内存。
		free(pb);
	}
	// 循环结束后，链表中的所有节点都被释放。
}
/************************************************************************
*	函数功能:延时函数
*	参数：
*		sec ：延时的秒数
* 	返回值:无
*************************************************************************/
// 函数定义：time_delay
// 功能：实现一个简单的延时功能，使程序暂停指定的秒数。
//      这是一个忙等待 (busy-waiting) 的延时，CPU 在此期间会持续检查时间，效率不高。
//      对于更精确或更低CPU占用的延时，应使用如 usleep (微秒) 或 sleep (秒) (在 <unistd.h>)
//      或者更高级的定时器机制。
// 参数：
//   int sec: 需要延时的总秒数。
// 返回值：
//   void: 该函数没有返回值。
void time_delay(int sec)
{
	// 声明两个 time_t 类型的变量 s_time (start_time) 和 e_time (end_time)。
	// time_t 通常是一个长整型，用于存储自 Epoch (1970年1月1日00:00:00 UTC) 以来的秒数。
	// 如果原代码用的int，且time()返回值是int，那么这里用int也可以，但time_t更标准。
	time_t s_time,e_time;
	// 调用 time(NULL) 函数获取当前的日历时间 (秒数)，并存入 s_time。
	s_time=time(NULL);
	// 进入一个无限循环 (while(1))，直到达到延时时间。
	while(1)
	{
		// 在循环中不断获取当前时间，存入 e_time。
		e_time=time(NULL);
		// 检查当前时间 e_time 是否已经达到或超过了 (s_time + sec)。
		// 即，是否已经过去了 sec 秒。
		if(e_time >= s_time+sec) // 使用 >= 更可靠，防止因为系统繁忙跳过精确的相等时刻
			// 如果达到了延时时间，使用 break 语句跳出 while 循环。
			break;
	}
	// 循环结束，延时完成。
}


/************************************************************************
*	函数功能:在屏幕的x,y位置，用color颜色打印一个字符串str
*	参数：
*		str：要打印的字符串的首地址
		x:显示在屏幕上的x坐标
		y:显示在屏幕上的y坐标
		color: 显示字符串的颜色
* 	返回值:无
*************************************************************************/
// 函数定义：color_pos_printf
// 功能：在控制台的指定坐标 (x, y) 处，以指定的颜色 (color) 打印一个字符串 (str)。
//      这个函数封装了移动光标、设置颜色和打印字符串三个动作。
// 参数：
//   char *str: 指向要打印的字符串的指针。
//   int x: 目标打印位置的列坐标 (水平)。
//   int y: 目标打印位置的行坐标 (垂直)。
//   int color: 要使用的颜色代码 (通常是定义在 "console.h" 中的宏，如 COLOR_RED)。
// 返回值：
//   void: 该函数没有返回值。
void color_pos_printf(char *str,int x,int y,int color)
{
	// 调用 cusor_moveto(x, y) 函数，将控制台光标移动到指定的 (x, y) 位置。
	// 这个函数通常在 "console.c" 中实现。
	cusor_moveto(x,y);
	// 调用 set_fg_color(color) 函数，设置后续打印文本的前景色 (字体颜色)。
	// 这个函数通常在 "console.c" 中实现。
	set_fg_color(color);
	// 调用 printf 函数打印字符串 str。
	// "%s\n": %s 打印字符串，\n 在字符串末尾添加一个换行符，使光标移到下一行的开头。
	// 如果不希望自动换行，可以去掉 \n。
	printf("%s\n",str);
	// 注意：原代码中cusor_moveto和set_fg_color后没有fflush(stdout)，
    // 如果这两个函数内部没有fflush，那么效果可能不会立即在所有终端上显现，
    // printf("%s\n") 因为带 \n 通常会触发刷新。
    // 为了确保光标移动和颜色设置立即生效，可能需要在 cusor_moveto 和 set_fg_color 内部
    // 或者在此函数末尾调用 fflush(stdout)。(假设它们内部已经处理)
}
/************************************************************************
*	函数功能:在屏幕上打印时间
*	参数：
*		m_time: 时间结构体
* 	返回值:无
*************************************************************************/
// 函数定义：time_printf
// 功能：将 TIME 结构体中存储的分钟和秒钟，格式化为 "mm:ss" 的形式，并在屏幕的固定位置以蓝色打印。
// 参数：
//   TIME m_time: 一个 TIME 类型的结构体变量 (不是指针)。TIME 结构体在 "lrc.h" 中定义，
//               包含 minute 和 second 两个 char 类型的成员。
// 返回值：
//   void: 该函数没有返回值。
void  time_printf(TIME m_time)
{
	// 声明一个字符数组 str，大小为30。用于存储格式化后的时间字符串。
	// 大小30对于 "mm:ss" 格式来说是足够的 ("00:00" 长度5，加上结束符 '\0' 是6)。
	char str[30];
	// 调用 sprintf 函数将 m_time 结构中的分钟和秒钟格式化成字符串，并存入 str 数组。
	// str: 目标字符串缓冲区。
	// "%02d:%02d": 格式控制字符串。
	//   %d: 表示输出一个十进制整数。
	//   02: 表示输出宽度至少为2，不足2位时在前面补0。
	//   例如，如果 m_time.minute 是 5，m_time.second 是 30，则 str 会变成 "05:30"。
	sprintf(str,"%02d:%02d",m_time.minute,m_time.second);

	// 调用之前定义的 color_pos_printf 函数，在屏幕的固定位置 (列40, 行5) 以蓝色打印格式化后的时间字符串 str。
	// COLOR_BLUE 是 "console.h" 中定义的颜色宏。
	color_pos_printf(str,40,5,COLOR_BLUE);
	//cusor_moveto(0,6); // 这行代码被注释掉了。原意可能是打印完时间后将光标移动到 (0,6) 位置。
}


/************************************************************************
*	函数功能:打印链表中5个节点的信息到屏幕上
*	参数：
*		start_point:打印的第一个节点
		pb: 要高亮的节点
* 	返回值:无
*************************************************************************/
// 函数定义：lrc_printf
// 功能：从指定的歌词链表节点 start_point 开始，在屏幕的特定位置连续打印5句歌词。
//       如果某句歌词是当前正在播放的歌词 (由 pb 指针指示)，则该句歌词会以红色高亮显示，其他歌词以蓝色显示。
// 参数：
//   LRC *start_point: 指向歌词链表中要开始打印的第一个节点的指针。
//   LRC *pb: 指向当前正在播放 (或应高亮显示) 的歌词节点的指针。
//            如果不需要高亮任何行，可以传入 NULL。
// 返回值：
//   void: 该函数没有返回值。
void lrc_printf(LRC *start_point , LRC *pb)
{
	// 声明一个整型变量 i，用作循环计数器，控制打印5句歌词。
	int i;
	// 使用 for 循环，迭代5次，每次打印一句歌词。
	for(i=0;i<5;i++)//从pb开始打印5句歌词
	{
		// 安全检查：如果 start_point 变为 NULL (例如链表剩余节点不足5个)，
		// 则使用 break 提前跳出循环，以防止访问 NULL 指针导致程序崩溃。
		if(start_point==NULL) break;

		// 检查当前要打印的歌词节点 start_point 是否就是应高亮显示的节点 pb。
		if(start_point==pb) //高亮红色显示节点
			// 如果是，则调用 color_pos_printf 在屏幕的 (列30, 行6+i) 位置，以红色 (COLOR_RED) 打印歌词。
			// start_point->lrc_buf: 访问 LRC 结构体中存储歌词文本的 lrc_buf 成员。
			// -> 是结构体指针访问成员的操作符。
			color_pos_printf(start_point->lrc_buf,30,6+i,COLOR_RED);
		else //普通蓝色显示
			// 如果不是应高亮的节点，则以蓝色 (COLOR_BLUE) 打印歌词。
			color_pos_printf(start_point->lrc_buf,30,6+i,COLOR_BLUE);

		// 将 start_point 指针移动到链表中的下一个节点，为下一次循环打印下一句歌词做准备。
		start_point=start_point->next;
	}
}

/*
	功能，在head指向的链表中， pb指向正在唱的节点，
	找要打印的5个节点的首节点的地址，然后返回其地址
*/
// 函数定义：find_start_point
// 功能：根据整个歌词链表的头节点 (head) 和当前正在播放的歌词节点 (pb)，
//       计算并返回一个指针，该指针指向在屏幕上显示的5句歌词中的第一句。
//       目标是使得当前播放的歌词 pb 尽量显示在这5行的中间位置 (通常是第3行)。
// 参数：
//   LRC *head: 指向整个歌词链表头节点的指针。
//   LRC *pb: 指向当前正在播放的歌词节点的指针。
// 返回值：
//   LRC*: 指向应该开始打印的5句歌词中的第一个节点的指针。
LRC * find_start_point(LRC *head,LRC*pb)
{
	// 如果 pb 为 NULL (例如歌曲刚开始，还没有定位到具体歌词行)，或者 head 为 NULL (链表为空)，
    // 则默认从链表头 head 开始显示。
	if(pb==NULL || head == NULL) return head;

	// 情况1：当前播放的歌词是链表的第一个节点 (pb == head)。
	if(pb==head)
		return head; // 从头节点开始显示 (pb 会在第一行)。
	// 情况2：当前播放的歌词是链表的第二个节点 (pb的前一个节点是头节点)。
	else if((pb->front)==head)
		return head; // 仍然从头节点开始显示 (pb 会在第二行)。

	// 为了让 pb 显示在第三行，我们需要从 pb 的前两个节点开始显示。
    // start_point = pb->front->front;
    // 但需要处理边界情况，即 pb 前面不足两个节点的情况。

	// 情况3：pb 接近链表尾部的情况，确保仍然显示5行。
	// 如果 pb 是尾节点 (pb->next == NULL)，则需要从 pb 前面第4个节点开始显示，才能使 pb 作为第5行。
	// (原逻辑是让pb在中间，但尾部处理逻辑似乎想让pb显示在最后几行)
	// 我们来调整逻辑，尽量让 pb 出现在第3行。
	// 如果 pb 是倒数第一个节点(尾节点): start_point = pb->front->front->front->front (如果存在)
	// 如果 pb 是倒数第二个节点: start_point = pb->front->front->front (如果存在)
	// ...

	// 修正后的逻辑：尝试让 pb 成为显示的第3行，除非 pb 离头部或尾部太近。

    // 尝试将 start_point 定位到 pb 的前两个节点
    LRC* potential_start = pb;
    int nodes_to_go_back = 2; // 目标是让 pb 成为第3行，所以往前数2个

    // 回溯查找 potential_start
    for (int k = 0; k < nodes_to_go_back; ++k) {
        if (potential_start->front != NULL) {
            potential_start = potential_start->front;
        } else {
            // 如果无法回溯足够多的节点 (即已到达链表头)，则直接从头开始
            return head;
        }
    }

    // 现在 potential_start 是 pb 前面的第2个节点 (如果存在)。
    // 接下来要检查从 potential_start 开始是否有足够的5个节点。
    // 如果不足5个节点，说明 pb 靠近尾部，需要调整 potential_start。
    LRC* temp_check = potential_start;
    int count_nodes = 0;
    for (int k = 0; k < 5; ++k) {
        if (temp_check != NULL) {
            count_nodes++;
            temp_check = temp_check->next;
        } else {
            break; // 到达链表尾部
        }
    }

    // 如果从 potential_start 开始不足5个节点，说明 pb 靠近尾部。
    // 此时需要将 potential_start 向前移动，使得能够显示5个节点，且最后一个节点是链表尾。
    if (count_nodes < 5) {
        // 回到原始的尾部判断逻辑，尝试从尾部倒数第5个开始
        // （如果链表总长度小于5，则从 head 开始）
        LRC* tail_walker = head;
        int total_nodes = 0;
        while(tail_walker != NULL) {
            total_nodes++;
            if (tail_walker->next == NULL) break; // tail_walker 现在是尾节点
            tail_walker = tail_walker->next;
        }

        if (total_nodes <= 5) return head; // 总节点数不足5，从头开始

        // 找到倒数第5个节点
        potential_start = tail_walker; // 从尾节点开始
        for(int k=0; k<4; ++k) { // 回溯4次找到倒数第5个
             if(potential_start->front) potential_start = potential_start->front;
             else break; // 以防万一
        }
        return potential_start;
    }

    // 如果上述条件都不满足，说明 pb 不在头部附近，也不在尾部附近，
    // 且从 pb->front->front 开始有足够的5个节点。
    return potential_start;

	/* 原来的尾部判断逻辑:
	else if(pb->next==NULL) // 如果 pb 是尾节点 (最后一个节点)
	{
		// 尝试找到 pb 前面第4个节点。 ((pb->front)->front)->front->front
		// 这个逻辑假设链表至少有5个节点。如果不足，这里会出错（访问NULL指针）。
		LRC* temp = pb;
		for(int k=0; k<4 && temp->front != NULL; ++k) temp = temp->front;
		return temp;
	}
	else if(pb->next->next==NULL) // 如果 pb 是倒数第二个节点
	{
		// 尝试找到 pb 前面第3个节点。 ((pb->front)->front)->front
		LRC* temp = pb;
		for(int k=0; k<3 && temp->front != NULL; ++k) temp = temp->front;
		return temp;
	}
	else // 其他情况 (pb 在中间，且其前后都有足够的节点)
		// 返回 pb 前面第2个节点，这样 pb 就会显示在第3行。
		return (pb->front)->front;
    */
}


// 函数定义：m_clock
// 功能：模拟一个时钟，将传入的 TIME 结构体中的秒数加1，并处理分钟的进位。
// 参数：
//   TIME *p_m_time: 指向 TIME 结构体的指针。函数将直接修改这个结构体的内容。
//                  TIME 结构体包含 minute 和 second 成员。
// 返回值：
//   void: 该函数没有返回值。
void m_clock(TIME *p_m_time)
{
		// 使用指针访问结构体成员：
		// (*p_m_time).second 这种写法是正确的，表示先解引用指针 p_m_time 得到 TIME 结构体，
		// 然后访问其 second 成员。
		// 或者更常用的写法是 p_m_time->second，这与 (*p_m_time).second 等价。

		// 将秒数加1。
		p_m_time->second++;
		// 检查秒数是否达到60。
		if(p_m_time->second==60)
		{
			// 如果秒数达到60，则将秒数归零。
			p_m_time->second=0;
			// 分钟数加1。
			p_m_time->minute++;
			// 检查分钟数是否达到60。
			if(p_m_time->minute==60)
				// 如果分钟数达到60，则将分钟数归零。
				// (这里没有处理小时的进位，假设歌词播放不会超过1小时，或者小时不重要)。
				p_m_time->minute=0;
		}
}

// 函数定义：song_msg_free
// 功能：释放之前为存储歌曲头部信息 (歌名、歌手等) 而动态分配的内存。
// 参数：
//   char *song_msg[]: 一个字符指针数组，其元素指向由 malloc 分配的字符串内存。
//                    这些字符串是 lrc_head_resolve 函数解析出来的歌曲信息。
// 返回值：
//   void: 该函数没有返回值。
void song_msg_free(char *song_msg[])
{
	// 声明一个整型变量 i，用作循环计数器。
	int i;
	// 使用 for 循环遍历 song_msg 数组的前4个元素。
	for(i=0;i<4;i++)
	{
		// 检查当前指针 song_msg[i] 是否不为 NULL。
		// 如果为 NULL，表示该项信息不存在或未分配内存，不需要释放。
		if(song_msg[i]!=NULL)
			// 如果指针不为 NULL，调用 free 函数释放其指向的内存。
			// 必须确保这里的指针确实是通过 malloc (或类似函数) 分配的。
			free(song_msg[i]);
			// 良好的实践是，在 free 之后将指针设为 NULL，防止悬挂指针。
            // song_msg[i] = NULL; (可选，但在这个函数上下文中，song_msg本身可能不再被使用)
	}
}







